# -*- coding: utf-8 -*-
import docx  # 用于读取 .docx 文件
import re    # 用于正则表达式操作
import pandas as pd  # 用于数据处理和格式化输出表格
import os    # 用于操作系统相关功能，如检查文件存在、获取环境变量
import json  # 用于加载和解析 JSON 配置文件
import google.generativeai as genai  # Google Gemini API 库
from collections import defaultdict # 导入 defaultdict，虽然当前版本未使用，但可能对未来扩展有用
import logging # 用于日志记录，方便调试

# --- 全局控制变量 ---
NEED_CALL_LLM_API = True # 全局开关：是否在脚本提取不足时尝试调用 LLM API (当前硬编码为 False)
                          # TODO: 可以考虑改为命令行参数或其他配置方式

# --- 基本日志设置 ---
# 配置日志记录器，设置级别为 INFO (信息)，以及日志格式
# 如果需要更详细的调试信息，可以将 level=logging.INFO 改为 level=logging.DEBUG
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - [%(funcName)s] - %(message)s')

# --- 配置加载与辅助函数 ---

def load_config(config_path='config.json'):
    """
    加载并执行 JSON 配置文件的基本验证。
    Args:
        config_path (str): 配置文件的路径。
    Returns:
        dict or None: 如果成功加载并验证，则返回配置字典；否则返回 None。
    """
    logging.info(f"从 {config_path} 加载配置")
    try:
        # 打开并读取配置文件
        with open(config_path, 'r', encoding='utf-8') as f:
            # 允许以 // 开头的注释行，先过滤掉
            config_lines = [line for line in f if not line.strip().startswith("//")]
            # 解析剩余的 JSON 文本
            config = json.loads("\n".join(config_lines))

        # --- 验证配置结构（基本）---
        # 检查必需的顶级键是否存在
        required_keys = ['api_key_env_var', 'model_name', 'fallback_threshold',
                         'comparison_operators', 'extraction_rules',
                         'formula_patterns', 'prompt_template']
        if not all(key in config for key in required_keys):
            # 如果缺少关键配置，则记录错误并引发异常
            raise ValueError(f"配置缺少一个或多个必需的键：{required_keys}")

        # --- 在加载时验证正则表达式模式的有效性 ---
        # 遍历需要包含正则表达式的配置列表
        for rule_list_key in ['extraction_rules', 'formula_patterns']:
            # 遍历列表中的每条规则
            for i, rule in enumerate(config.get(rule_list_key, [])):
                # 如果规则包含 'pattern' 键
                if 'pattern' in rule and rule['pattern'] is not None: # 确保模式存在且不为None
                    try:
                        # 尝试编译正则表达式，检查语法是否正确
                        re.compile(rule['pattern'], re.IGNORECASE)
                    except re.error as e:
                        # 如果编译失败，记录错误，并将该模式设为 None 以禁用它
                        logging.error(f"{rule_list_key}[{i}] 中的正则表达式模式无效 ('{rule.get('name', 'N/A')}')：{rule['pattern']} -> {e}")
                        rule['pattern'] = None  # 禁用无效规则

        logging.info("配置加载成功。")
        return config # 返回加载并验证后的配置字典
    except FileNotFoundError:
        logging.error(f"未找到配置文件 '{config_path}'。")
    except json.JSONDecodeError as e:
        logging.error(f"'{config_path}' 中的 JSON 格式无效：{e}")
    except Exception as e:
        logging.error(f"加载配置时出错：{e}")
    return None # 加载失败返回 None

def normalize_text(text, config):
    """
    规范化文本：处理多余空格并将配置文件中定义的比较运算符替换为其 ASCII 等效项。
    Args:
        text (str): 需要规范化的输入文本。
        config (dict): 包含 'comparison_operators' 映射的配置字典。
    Returns:
        str: 规范化后的文本。
    """
    # 检查输入是否为有效字符串
    if not text or not isinstance(text, str): return text
    # 使用正则表达式将多个空格替换为单个空格，并去除首尾空格
    text = re.sub(r'\s+', ' ', text).strip()
    # 获取配置中的比较运算符映射
    operators = config.get('comparison_operators', {})
    # 遍历映射，将 Unicode 运算符替换为 ASCII 等效项
    for op_unicode, op_ascii in operators.items():
        text = text.replace(op_unicode, op_ascii)
    return text # 返回处理后的文本

def table_to_markdown(table, config):
    """
    将 python-docx 的 Table 对象转换为 Markdown 表格字符串，并对单元格内容应用文本规范化。
    Args:
        table (docx.table.Table): 输入的表格对象。
        config (dict): 用于 normalize_text 的配置字典。
    Returns:
        str: Markdown 格式的表格字符串，或错误信息。
    """
    md = "" # 初始化 Markdown 字符串
    try:
        rows_data = [] # 存储表格所有行的数据
        max_cols = 0   # 记录表格的最大列数
        # 遍历表格的每一行
        for row in table.rows:
            # 提取每个单元格的文本，进行规范化，并处理单元格不存在 text 属性的情况
            cells = [normalize_text(cell.text, config) for cell in row.cells if hasattr(cell, 'text')]
            rows_data.append(cells)
            # 更新最大列数
            if len(cells) > max_cols: max_cols = len(cells)

        # 如果表格无数据或无法读取表头，则返回错误信息
        if not rows_data or not rows_data[0] or max_cols == 0:
            return "[空表格或无法读取表头]\n"

        # --- 构建 Markdown 表格 ---
        # 确保所有行具有相同的列数（填充短行，截断长行）
        for row in rows_data:
            if len(row) < max_cols: row.extend([''] * (max_cols - len(row))) # 填充
            elif len(row) > max_cols: row = row[:max_cols] # 截断

        header = rows_data[0] # 第一行为表头
        separator = ["---"] * max_cols # Markdown 分隔行
        body = rows_data[1:] # 其余行为表格主体

        # 拼接 Markdown 字符串
        md += "| " + " | ".join(header) + " |\n"
        md += "| " + " | ".join(separator) + " |\n"
        for row_cells in body:
            md += "| " + " | ".join(row_cells) + " |\n"

    except Exception as e:
        # 记录转换过程中的任何错误
        logging.warning(f"表格转换为 Markdown 时出错：{e}")
        return "[表格转换错误]\n"
    return md # 返回 Markdown 格式的表格

def find_column_index(headers, keywords):
    """
    根据关键字列表（作为不区分大小写的正则表达式）在表头列表中查找第一个匹配的列索引。
    Args:
        headers (list[str]): 表头单元格文本列表。
        keywords (list[str]): 用于匹配表头的正则表达式模式列表。
    Returns:
        int: 第一个匹配的列索引，如果未找到则返回 -1。
    """
    # 检查输入是否有效
    if not headers or not keywords: return -1
    # 遍历每个关键字模式
    for keyword_pattern in keywords:
        try:
            # 遍历每个表头及其索引
            for idx, header in enumerate(headers):
                # 使用不区分大小写的正则搜索来匹配关键字
                if re.search(keyword_pattern, header, re.IGNORECASE):
                    return idx # 找到匹配项，返回索引
        except re.error as e:
            # 如果关键字模式是无效的正则表达式，记录警告
            logging.warning(f"无效的关键字正则表达式模式 '{keyword_pattern}'：{e}")
    return -1 # 遍历完所有关键字和表头都未找到匹配项

# --- 正则表达式应用引擎 ---

def apply_regex_rules(text, rules_config, target_element_type):
    """
    将配置中定义的、适用于目标元素类型（段落、表格单元格、任何）的正则表达式规则应用于给定文本。
    Args:
        text (str): 要应用规则的输入文本。
        rules_config (list[dict]): 从配置文件加载的 'extraction_rules' 列表。
        target_element_type (str): 'paragraph' 或 'table_cell'，指示当前处理的元素类型。
    Returns:
        list[dict]: 包含每个成功匹配结果的列表，每个结果是一个字典，包含规则名称和匹配数据。
    """
    results = [] # 存储匹配结果
    if not text: return results # 如果文本为空，直接返回

    # 筛选出适用于当前目标元素类型且模式有效的规则
    applicable_rules = [
        rule for rule in rules_config
        if rule.get("rule_type") == "regex"
           and rule.get("target_element") in [target_element_type, "any"]
           and rule.get("pattern") # 确保模式有效（加载时未因错误而被设为 None）
    ]

    # 遍历适用的规则
    for rule in applicable_rules:
        pattern = rule['pattern']
        rule_name = rule.get("name", "未命名的正则表达式")
        context_keywords = rule.get("context_keywords", [])
        value_format = rule.get("value_format") # 值格式化字符串

        try:
            # --- 上下文检查 ---
            # 如果定义了上下文关键字，则检查当前文本是否包含其中任何一个
            context_ok = not context_keywords or any(re.search(kw, text, re.IGNORECASE) for kw in context_keywords)
            if not context_ok: continue # 如果上下文不匹配，则跳过此规则

            # --- 执行正则匹配 ---
            # 使用 finditer 查找文本中所有不重叠的匹配项
            for match in re.finditer(pattern, text, re.IGNORECASE):
                # 获取命名捕获组（如果存在）
                match_dict = match.groupdict()
                # 初始化匹配数据结构
                match_data = {"name": None, "value": None, "is_suffix": rule.get("is_suffix", False)}

                # --- 构建/提取指标值 ---
                constructed_value = None
                # 优先级 1：使用 value_format 字符串（如果提供）
                if value_format:
                    try:
                        # 查找格式字符串中使用的键
                        format_keys = re.findall(r'{(\w+)}', value_format)
                        # 从匹配的命名组中获取这些键的值，如果键不存在则使用空字符串
                        format_dict = {k: match_dict.get(k, '') for k in format_keys}
                        # 应用格式化
                        constructed_value = value_format.format(**format_dict)
                    except KeyError as e:
                        logging.warning(f"规则 '{rule_name}' 中的格式化键错误：{e}。使用完整匹配。")
                        constructed_value = match.group(0) # 格式化失败，回退到完整匹配文本
                    except Exception as e:
                        logging.warning(f"应用 value_format 时出错，规则 '{rule_name}'：{e}。使用完整匹配。")
                        constructed_value = match.group(0)

                # 优先级 2：根据常见的命名组构建值（如果未使用 format 字符串）
                elif 'low' in match_dict and 'high' in match_dict: # 处理范围
                    unit = match_dict.get('unit', '')
                    constructed_value = f"{match_dict['low']} ~ {match_dict['high']} {unit}".strip()
                elif 'value' in match_dict and 'tolerance' in match_dict: # 处理公差
                    unit = match_dict.get('unit', '')
                    constructed_value = f"{match_dict['value']} ± {match_dict['tolerance']} {unit}".strip()
                elif 'value' in match_dict and 'operator' in match_dict: # 处理带操作符的值
                    unit = match_dict.get('unit', '')
                    constructed_value = f"{match_dict.get('operator','')}{match_dict['value']} {unit}".strip()
                elif 'value' in match_dict: # 处理简单值（可能带单位）或文本值（如 "不得检出"）
                    unit = match_dict.get('unit', '')
                    constructed_value = f"{match_dict['value']} {unit}".strip()

                # 优先级 3：回退到使用整个匹配的文本作为值
                if constructed_value is None:
                    constructed_value = match.group(0)

                # 存储构建/提取的值
                match_data["value"] = constructed_value.strip()

                # --- 提取指标名称 ---
                if 'name' in match_dict:
                    match_data["name"] = match_dict['name'].strip()

                # --- 添加结果 ---
                # 仅当提取到名称或值时才认为匹配有效
                if match_data.get("name") or match_data.get("value"):
                    results.append({
                        "rule_name": rule_name, # 记录应用了哪个规则
                        "match_data": match_data # 存储提取的数据
                    })

        except re.error as e:
            logging.warning(f"规则 '{rule_name}' 中的正则表达式执行错误：{e}")
        except Exception as e:
            # 捕获应用规则时的其他潜在错误
            logging.warning(f"将正则表达式规则 '{rule_name}' 应用于文本 '{text[:50]}...' 时出错：{e}")

    return results # 返回所有匹配结果的列表

# --- 表格解析引擎 ---

def identify_table_type_from_config(table, preceding_text, config):
    """
    通过检查配置文件中的表格解析规则来识别表格类型。
    Args:
        table (docx.table.Table): 要识别的表格对象。
        preceding_text (str): 表格之前的段落文本（用于上下文判断）。
        config (dict): 包含 'extraction_rules' 的配置字典。
    Returns:
        tuple(str, dict or None): 返回识别到的表格类型名称（规则名）和对应的规则配置字典，如果未识别则返回 ("未知", None)。
    """

    logging.debug(f"识别表格类型")

    # 从配置中筛选出表格解析器规则
    table_rules = [rule for rule in config.get('extraction_rules', []) if rule.get('rule_type') == 'table_parser']

    logging.debug(f"识别表格类型，前面的文本：'{preceding_text[:50]}...'")
    logging.debug(f"找到 {len(table_rules)} 个表格解析器规则")
    # 如果没有找到表格解析器规则，则返回未知
    if not table_rules:
        logging.debug("没有找到表格解析器规则")
        return "未知", None

    # 遍历每个表格解析器规则
    for rule in table_rules:
        identification = rule.get('identification', {}) # 获取识别关键字配置
        context_keywords = identification.get('context_keywords', []) # 上下文关键字
        header_keywords = identification.get('header_keywords', []) # 表头关键字
        rule_name = rule.get("name", "未知的表格规则")

        # --- 检查上下文关键字 ---
        # 如果没有上下文关键字，则认为匹配；否则，检查 preceding_text 是否匹配任何一个关键字
        context_match = (not context_keywords) or any(re.search(kw, preceding_text, re.IGNORECASE) for kw in context_keywords)

        # --- 检查表头关键字 ---
        header_match = True # 默认表头匹配（如果没有指定 header_keywords）
        if header_keywords: # 仅在提供了表头关键字时检查
            try:
                # 确保表格至少有一行（表头）
                if not table.rows: raise IndexError("表格没有行")
                # 将表头单元格文本连接起来，并进行规范化
                header_text = " | ".join(normalize_text(cell.text, config) for cell in table.rows[0].cells)
                # 检查连接后的表头文本是否匹配任何一个表头关键字
                header_match = any(re.search(kw, header_text, re.IGNORECASE) for kw in header_keywords)
            except IndexError: header_match = False # 没有表头行
            except Exception as e:
                logging.warning(f"读取规则 '{rule_name}' 的表头时出错：{e}")
                header_match = False # 读取表头出错，认为不匹配

        # --- 判断是否匹配 ---
        # 如果上下文匹配（或未指定）且表头匹配（或未指定），则认为此规则适用
        if context_match and header_match:
            logging.debug(f"根据上下文 '{preceding_text[:30]}...' 和/或表头，将表格识别为类型：'{rule_name}'")
            # 返回规则名称和完整的规则配置字典
            return rule_name, rule
        
    # 如果遍历完所有规则都未找到匹配项
    logging.debug(f"无法根据配置识别表格类型（前面的文本：'{preceding_text[:50]}...'）")
    return "未知", None # 返回未识别状态

def parse_configured_table(table, table_rule_config, section_context, config, found_formulas):
    """
    根据其识别出的配置规则来解析表格，提取指标。
    Args:
        table (docx.table.Table): 要解析的表格对象。
        table_rule_config (dict): 匹配到的表格解析规则的配置字典。
        section_context (str): 当前推断的章节目录或上下文标签。
        config (dict): 完整的配置字典。
        found_formulas (dict): 已找到的公式及其关联关键字的字典。
    Returns:
        list[dict]: 从表格中提取的指标字典列表。
    """
    metrics = [] # 存储提取的指标
    parsing_conf = table_rule_config.get('parsing_config', {}) # 获取具体解析配置
    structure_type = parsing_conf.get('structure_type') # 获取表格结构类型
    table_name = table_rule_config.get('name', '未命名的表格')
    logging.debug(f"使用结构 '{structure_type}' 解析表格 '{table_name}'（上下文：{section_context}）")

    # --- 安全地读取表格数据 ---
    table_rows = [] # 存储所有行的单元格数据
    max_cols = 0    # 表格最大列数
    try:
        for row in table.rows:
            cells = [normalize_text(cell.text, config) for cell in row.cells if hasattr(cell, 'text')]
            table_rows.append(cells)
            if len(cells) > max_cols: max_cols = len(cells)
        # 填充短行，确保所有行长度一致，便于索引
        for row in table_rows:
            if len(row) < max_cols: row.extend([''] * (max_cols - len(row)))
    except Exception as e:
        # 记录读取表格时的严重错误
        logging.error(f"读取表格 '{table_name}' 的行时出现严重错误：{e}")
        return metrics # 返回空列表

    # 至少需要一行数据（表头或数据）
    if len(table_rows) < 1: return metrics
    # 假设第一行是表头（如果存在）
    headers = table_rows[0] if table_rows else []
    # 其余行是数据行
    data_rows = table_rows[1:]

    # --- 应用解析逻辑 ---
    try:
        # --- 结构类型 1: 简单的键值对表格 (例如 安全指标表) ---
        if structure_type == "simple_key_value":
            # 根据配置中的关键字查找项目名称列和指标值列的索引
            item_col_idx = find_column_index(headers, parsing_conf.get('item_column_keywords', []))
            value_col_idx = find_column_index(headers, parsing_conf.get('value_column_keywords', []))
            default_unit = parsing_conf.get('default_unit') # 获取默认单位（如果配置了）

            # 确保找到了项目列和值列
            if item_col_idx != -1 and value_col_idx != -1:
                # 准备应用于值单元格的后处理正则表达式规则
                # 包括配置文件中 target 为 'table_cell' 或 'any' 的通用规则
                value_rules = [r for r in config.get('extraction_rules', []) if r.get('rule_type') == 'regex' and r.get('target_element') in ['table_cell', 'any']]
                # 再加上此表格配置中特定的 value_post_processing 规则
                value_rules.extend(parsing_conf.get('value_post_processing', []))

                # 遍历数据行
                for row in data_rows:
                    # 确保行长度足够访问所需的列
                    if len(row) > max(item_col_idx, value_col_idx):
                        name = row[item_col_idx]      # 提取项目名称
                        value_cell_text = row[value_col_idx] # 提取原始值单元格文本
                        processed_value = value_cell_text # 默认使用原始文本

                        # --- 应用值后处理规则 ---
                        # 对原始值单元格文本应用配置的正则表达式规则
                        regex_results = apply_regex_rules(value_cell_text, value_rules, 'table_cell')
                        # 如果有匹配结果
                        if regex_results:
                            # 优先使用第一个匹配结果中名为 'value' 的捕获组
                            best_val = next((res['match_data'].get('value') for res in regex_results if res['match_data'].get('value')), None)
                            # 如果找到 'value' 组，则使用它；否则回退到第一个匹配规则的完整匹配（如果需要）
                            # 注意：这里可能需要根据规则配置决定是否回退到完整匹配
                            processed_value = best_val if best_val is not None else regex_results[0].get('match_data', {}).get('value', value_cell_text)

                        # --- 添加默认单位 ---
                        # 如果配置了默认单位，且处理后的值看起来是纯数字（可能带操作符），则添加单位
                        if default_unit and processed_value and re.match(r"^[<>≤≥=]?\s*\d+(\.\d+)?$", processed_value.strip()):
                             processed_value = f"{processed_value.strip()} {default_unit}"

                        # --- 添加到结果列表 ---
                        # 仅当名称和处理后的值都有效时才添加
                        if name and processed_value and processed_value != '-':
                            # 查找关联的公式
                            formula = next((f for kw, f in found_formulas.items() if kw in name.lower()), None)
                            metrics.append({'指标名称': name, '章节目录': section_context, '指标数值': processed_value, '指标公式': formula or '-'})

        # --- 结构类型 2: 类别-等级-值 表格 (例如 感官要求表) ---
        elif structure_type == "category_grade_value":
            # 查找类别列索引
            cat_col_idx = find_column_index(headers, parsing_conf.get('category_column_keywords', []))
            # 查找每个等级对应的列索引
            grade_col_indices = {
                grade: find_column_index(headers, keywords)
                for grade, keywords in parsing_conf.get('grade_columns', {}).items()
            }
            # 过滤掉未找到列的等级
            grade_col_indices = {g: idx for g, idx in grade_col_indices.items() if idx != -1}

            # 确保找到了类别列和至少一个等级列
            if cat_col_idx != -1 and grade_col_indices:
                # 获取分类特定处理规则的映射
                category_rules_map = parsing_conf.get('category_specific_processing', {})
                # 获取通用的单元格处理规则
                general_cell_rules = [r for r in config.get('extraction_rules', []) if r.get('rule_type') == 'regex' and r.get('target_element') in ['table_cell', 'any']]

                # 遍历数据行
                for row in data_rows:
                    # 确保行长度足够访问类别列
                    if len(row) > cat_col_idx:
                        category = row[cat_col_idx] # 获取类别名称
                        if not category: continue # 跳过类别为空的行

                        # --- 查找适用于此类别行的特定处理规则 ---
                        row_category_rules = [] # 存储适用于当前行的规则
                        for cat_key_pattern, rules in category_rules_map.items():
                            try:
                                # 使用正则匹配类别名称
                                if re.search(cat_key_pattern, category, re.IGNORECASE):
                                    row_category_rules.extend(rules) # 添加匹配到的规则列表
                            except re.error as e: logging.warning(f"表格 '{table_name}' 中的分类正则表达式无效 '{cat_key_pattern}'：{e}")

                        # --- 处理每个配置的等级列 ---
                        for grade, col_idx in grade_col_indices.items():
                            # 确保行长度足够访问当前等级列
                            if len(row) > col_idx:
                                value_cell_text = row[col_idx] # 获取等级对应的单元格原始文本
                                if not value_cell_text: continue # 跳过空单元格

                                processed_value = value_cell_text # 默认值
                                metric_name_suffix = category # 默认名称后缀
                                metric_name_override = None    # 是否由规则覆盖整个名称
                                rule_applied = False           # 标记是否有规则被应用

                                # --- 首先尝试应用分类特定规则 ---
                                if row_category_rules:
                                    regex_results = apply_regex_rules(value_cell_text, row_category_rules, 'table_cell')
                                    # 如果有匹配结果
                                    if regex_results:
                                        best_match_data = regex_results[0]['match_data'] # 取第一个匹配的结果数据
                                        processed_value = best_match_data.get('value', value_cell_text) # 获取处理后的值
                                        # 如果规则结果包含名称，判断是后缀还是完整名称
                                        rule_metric_name = best_match_data.get('name')
                                        if rule_metric_name:
                                            if best_match_data.get('is_suffix'): # 规则指定名称是后缀
                                                metric_name_suffix = rule_metric_name
                                            else: # 规则指定名称是完整名称
                                                metric_name_override = rule_metric_name
                                        rule_applied = True # 标记已应用规则

                                # --- 如果没有应用特定规则，尝试通用单元格规则 ---
                                if not rule_applied and general_cell_rules:
                                    regex_results = apply_regex_rules(value_cell_text, general_cell_rules, 'table_cell')
                                    if regex_results:
                                        # 优先使用 'value' 组，如果规则没有 'value' 组，则保留原始文本
                                        best_val = next((res['match_data'].get('value') for res in regex_results if res['match_data'].get('value')), None)
                                        processed_value = best_val if best_val is not None else value_cell_text

                                # --- 构建最终指标名称 ---
                                metric_name = metric_name_override if metric_name_override else f"{grade}-{metric_name_suffix}"

                                # --- 添加到结果列表 ---
                                if metric_name and processed_value and processed_value != '-':
                                    formula = next((f for kw, f in found_formulas.items() if kw in metric_name.lower()), None)
                                    metrics.append({'指标名称': metric_name, '章节目录': section_context, '指标数值': processed_value, '指标公式': formula or '-'})

        # --- 结构类型 3: 直接映射 (例如 茎粗分类表) ---
        elif structure_type == "direct_mapping":
            # 从配置中获取直接映射列表
            mapping = parsing_conf.get('mapping', [])
            # 遍历映射项
            for item in mapping:
                # 确保包含必需的键
                if '指标名称' in item and '指标数值' in item:
                    metrics.append({
                        '指标名称': item['指标名称'],
                        '章节目录': section_context,
                        '指标数值': normalize_text(item['指标数值'], config), # 对映射值也进行规范化
                        '指标公式': item.get('指标公式', '-')
                    })

    except Exception as e:
        # 记录解析表格时的顶层错误
        logging.error(f"解析表格 '{table_name}'（类型：{structure_type}）时出错：{e}", exc_info=True)

    return metrics # 返回从该表格提取的指标列表

# --- 主要提取协调器 ---

def extract_metrics_with_script(doc, config):
    """
    从 DOCX 文档中提取指标，由配置驱动。
    Args:
        doc (docx.document.Document): 已加载的 python-docx 文档对象。
        config (dict): 加载的配置字典。
    Returns:
        list[dict]: 提取出的指标字典列表。
    """
    metrics = [] # 存储所有提取的指标
    found_formulas = {} # 存储找到的公式 {关键字: 公式文本}
    current_section = "未知" # 当前处理的章节号
    processed_tables = set() # 存储已处理表格对象的 ID，防止重复处理
    extraction_rules = config.get('extraction_rules', []) # 获取提取规则列表
    formula_patterns = config.get('formula_patterns', []) # 获取公式模式列表
    logging.info("开始基于脚本的提取...")

    # --- 第一遍：遍历段落查找公式和章节上下文 ---
    logging.info("开始第一遍：查找公式和章节...")

    for para in doc.paragraphs:
        # 规范化段落文本
        text = normalize_text(para.text, config)
        if not text: continue # 跳过空段落
        # --- 更新当前章节号 ---
        # 尝试匹配段落开头的章节编号模式
        match_section = re.match(r"^\s*(\d+(?:\.\d+)*)\s+", text)
        if match_section:
            current_section = match_section.group(1).strip() # 获取章节号

        # --- 查找公式 ---
        # 遍历配置中的公式模式
        for f_pattern in formula_patterns:
            pattern = f_pattern.get('pattern') # 获取正则表达式
            keywords = f_pattern.get('associated_metric_keywords', []) # 获取关联关键字
            if pattern: # 确保模式有效
                try:
                    # 在当前段落文本中搜索公式模式
                    match = re.search(pattern, text, re.IGNORECASE)
                    # 如果找到匹配且包含名为 'formula' 的捕获组
                    if match and 'formula' in match.groupdict():
                        formula_text = match.group('formula').strip() # 提取公式文本
                        logging.debug(f"在章节 {current_section} 中找到公式 '{formula_text}'")
                        # 将公式与关联的关键字（小写）存储在字典中
                        for keyword in keywords:
                            found_formulas[keyword.lower()] = formula_text
                except re.error as e: logging.warning(f"公式正则表达式模式错误：{e}")
                except Exception as e: logging.warning(f"公式正则表达式应用错误：{e}")

    # --- 第二遍：遍历文档元素（段落和表格）进行指标提取 ---
    logging.info("开始第二遍：处理段落和表格...")
    current_section = "未知" # 重置当前章节
    last_para_text = ""      # 存储上一个段落的文本，用于表格上下文判断
    # 获取文档主体中的所有元素（段落 p 和表格 tbl）
    doc_elements = list(doc.element.body)

    # 遍历每个元素
    for i, element in enumerate(doc_elements):
        # 获取元素的类型（'p' 或 'tbl'）
        element_type = element.tag.split('}')[-1]
        logging.info(f"处理元素 {i+1}/{len(doc_elements)}：类型 '{element_type}'")

        # --- 处理段落 ---
        if element_type == 'p':
            # 将 OXML 元素包装成 Paragraph 对象
            para = docx.text.paragraph.Paragraph(element, doc)
            current_para_text_orig = para.text.strip() # 获取原始文本
            if not current_para_text_orig: continue # 跳过空段落
            # 存储当前段落文本作为下一个表格的潜在上下文
            last_para_text = current_para_text_orig
            logging.info(f"处理段落：'{current_para_text_orig[:50]}...'")

            # 对文本进行规范化
            current_para_text_norm = normalize_text(current_para_text_orig, config)
            logging.info(f"规范化段落：'{current_para_text_norm[:50]}...'")

            # --- 更新当前章节 ---
            match_section = re.match(r"^\s*(\d+(?:\.\d+)*)\s+", current_para_text_orig)
            if match_section:
                current_section = match_section.group(1).strip()

            # --- 应用针对段落的正则表达式规则 ---
            # 调用 apply_regex_rules 函数，目标类型为 'paragraph'
            para_results = apply_regex_rules(current_para_text_norm, extraction_rules, 'paragraph')
            # 处理匹配结果
            for result in para_results:
                metric_data = result['match_data'] # 获取提取的数据
                name = metric_data.get('name')     # 获取指标名称
                value = metric_data.get('value')    # 获取指标值
                # 如果名称和值都有效
                if name and value:
                    # 查找关联的公式
                    formula = next((f for kw, f in found_formulas.items() if kw in name.lower()), None)
                    # 添加到指标列表
                    metrics.append({'指标名称': name, '章节目录': current_section, '指标数值': value, '指标公式': formula or '-'})

        # --- 处理表格 ---
        elif element_type == 'tbl':
            # 使用表格对象的 id 检查是否已处理过，避免重复
            if id(element) in processed_tables: continue
            # 将 OXML 元素包装成 Table 对象
            table = docx.table.Table(element, doc)
            # 将此表格标记为已处理
            processed_tables.add(id(element))

            # --- 使用配置识别表格类型 ---
            # 调用识别函数，传入表格对象、前一个段落的文本和配置
            table_type_name, table_rule_config = identify_table_type_from_config(table, last_para_text, config)

            # 如果成功识别出类型且找到了对应的配置规则
            if table_rule_config:
                # 创建上下文标签，包含章节号和表格类型名称
                context_label = f"{current_section} ({table_type_name})"
                # 调用配置驱动的表格解析函数
                table_metrics = parse_configured_table(table, table_rule_config, context_label, config, found_formulas)
                # 将从表格中提取的指标添加到总列表中
                metrics.extend(table_metrics)
            else:
                # 如果未识别表格类型，记录调试信息
                logging.debug(f"跳过类型/配置未知的表格（前面的文本：'{last_para_text[:50]}...'）")

    # --- 去重 ---
    # 对提取到的所有指标进行去重处理
    logging.debug("应用去重...")
    unique_metrics_tuples = set() # 存储唯一指标的标识符元组
    final_metrics = [] # 存储去重后的指标列表
    for m in metrics:
        # 创建标识符：(名称, 基础章节号, 值)
        # 从章节目录中提取基础章节号（去除括号和斜杠后的部分）
        base_section = m.get('章节目录','未知').split('/')[0].strip().split('(')[0].strip()
        identifier = (m.get('指标名称',''), base_section, m.get('指标数值',''))
        # 如果此标识符未见过
        if identifier not in unique_metrics_tuples:
            final_metrics.append(m) # 添加到最终列表
            unique_metrics_tuples.add(identifier) # 记录此标识符
        # else:
             # logging.debug(f"跳过重复的指标：{identifier}") # 可以取消注释以查看跳过了哪些

    logging.info(f"[正则匹配]脚本提取完成。找到 {len(final_metrics)} 个唯一指标。")
    return final_metrics # 返回最终的指标列表

# --- LLM 交互函数 ---

def call_llm_api(filepath, config):
    """
    准备数据并调用配置的 LLM API，请求 JSON 格式的响应。
    Args:
        filepath (str): 输入文档的文件路径。
        config (dict): 加载的配置字典。
    Returns:
        str or None: LLM API 返回的原始文本响应（预期为 JSON 字符串），或在出错时返回 None。
    """
    logging.info("准备数据以纯文本方式提取...")
    try:
        # 加载文档以提取原始上下文
        doc = docx.Document(filepath)
        # 提取文本片段：选择包含数字或特定关键字的段落的前 500 个字符
        text_snippets = []
        for para in doc.paragraphs:
            p_text = para.text.strip()
            # 简单的启发式筛选相关段落
            if p_text and (re.search(r'\d', p_text) or re.search(r'表\s?\d+|要求|指标|限值|分类|判定', p_text, re.IGNORECASE)):
                text_snippets.append(normalize_text(p_text, config)) # 发送规范化后的文本

        # 将所有表格转换为 Markdown 格式
        markdown_tables = [table_to_markdown(table, config) for table in doc.tables]

        # --- 配置和调用 API ---
        # 从环境变量获取 API Key
        api_key = config['api_key_env_var']
  
        # 配置 Google AI 客户端
        genai.configure(api_key=api_key)
        # 创建生成模型实例，指定模型名称和请求 JSON 响应
        model = genai.GenerativeModel(
            config.get('model_name', 'gemini-1.5-pro-latest'), # 使用配置的模型或默认值
            generation_config={"response_mime_type": "application/json"} # 请求 JSON
        )

        # --- 构建 Prompt ---
        # 使用配置中的 prompt_template，并填充提取的数据
        prompt = config['prompt_template'].format(
            doc_filename=os.path.basename(filepath), # 文件名
            text_snippets="\n---\n".join(text_snippets), # 文本片段
            markdown_tables="\n\n".join(markdown_tables) # Markdown 表格
        )

        logging.info(f"调用--->Pandas DataFrame...")
        # logging.debug(f"LLM 提示（前 500 个字符）：{prompt[:500]}") # 取消注释以调试 Prompt
        # 调用模型生成内容
        response = model.generate_content(prompt)
        logging.info("调用完成。")
        # logging.debug(f"LLM 原始响应：{response.text}") # 取消注释以查看原始响应
        # 返回 LLM 的文本响应（预期为 JSON 字符串）
        return response.text

    except Exception as e:
        # 记录调用 API 过程中的任何错误
        logging.error(f"调用时出错：{e}", exc_info=True)
        return None # 出错时返回 None


def parse_llm_response(response_text):
    """Parses response, expecting JSON format."""
    logging.info("解析结果 (JSON)...") # Updated log message
    if not response_text: return None
    try:
        # --- Clean potential markdown backticks around JSON ---
        cleaned_text = response_text.strip()
        if cleaned_text.startswith("```json"):
            logging.debug("Cleaning ```json wrapper from response.")
            cleaned_text = cleaned_text[7:-3].strip()
        elif cleaned_text.startswith("```"):
            logging.debug("Cleaning ``` wrapper from response.")
            cleaned_text = cleaned_text[3:-3].strip()

        # --- Parse JSON ---
        logging.debug(f"Attempting to parse JSON: {cleaned_text[:200]}...") # Log before parsing
        metrics = json.loads(cleaned_text) # Parse JSON string

        # --- Validate Structure ---
        # Check if it's a list of dictionaries
        if isinstance(metrics, list) and all(isinstance(item, dict) for item in metrics):
            processed_metrics = [] # Store validated metrics
            required_keys = ['指标名称', '指标数值'] # Minimum required keys
            # Iterate through items received from LLM
            for item in metrics:
                # Check if minimum required keys are present
                if all(k in item for k in required_keys):
                    # Build the metric dictionary with defaults for optional keys
                    processed_metrics.append({
                        '指标名称': item['指标名称'],
                        '指标数值': item['指标数值'],
                        '章节目录': item.get('章节目录', ''), # Use .get for optional keys
                        '指标公式': item.get('指标公式', '-') or '-' # Use .get and ensure '-' if empty/null
                    })
                else:
                    # Log if an item is missing required keys
                    logging.warning(f"metric object missing required keys: {item}")

            logging.info(f"成功解析 {len(processed_metrics)} 个指标")
            return processed_metrics # Return the validated list
        else:
            # Log if the overall structure is not a list of dictionaries
            logging.warning(f"response is not a valid JSON list of objects: {cleaned_text[:200]}...")
            return None # Indicate parsing failure
    except json.JSONDecodeError as e:
        # Log JSON parsing errors specifically
        logging.error(f"Failed to decode JSON response: {e}")
        logging.error(f"Original Response Text (before cleaning):\n{response_text}")
        return None # Indicate parsing failure
    except Exception as e:
        # Log any other unexpected errors during processing
        logging.error(f"Error processing response: {e}", exc_info=True)
        return None # Indicate parsing failure
    
# --- 显示函数 ---

def display_results(metrics_list, source):
    """
    使用 Pandas DataFrame 将提取的指标列表格式化并打印到控制台。
    Args:
        metrics_list (list[dict]): 要显示的指标字典列表。
        source (str): 指标的来源描述 (例如 "脚本规则提取", "LLM 提取")。
    """
    # 如果列表为空，则记录信息并打印提示
    if not metrics_list:
        logging.info(f"没有来自源 {source} 的指标可显示")
        print(f"\n--- 未找到任何指标 ({source}) ---")
        return

    print(f"\n--- 指标提取结果 ({source}) ---")
    try:
        # 将指标列表转换为 Pandas DataFrame
        df = pd.DataFrame(metrics_list)
        # --- 确保必需的列存在并设置显示选项 ---
        all_cols = ['指标名称', '章节目录', '指标数值', '指标公式']
        # 如果 DataFrame 中缺少某个列，则添加该列并填充空字符串
        for col in all_cols:
            if col not in df.columns: df[col] = ''
        # 重新排序列顺序
        df = df[all_cols]
        # 填充 '指标公式' 列的空值（NaN）为 '-'
        df['指标公式'] = df['指标公式'].fillna('-')
        # 填充其他所有列的空值（NaN）为空字符串
        df = df.fillna('')

        # --- 配置 Pandas 显示选项 ---
        pd.set_option('display.max_rows', None) # 显示所有行
        pd.set_option('display.max_columns', None) # 显示所有列
        pd.set_option('display.width', 180) # 设置显示宽度
        pd.set_option('display.colheader_justify', 'left') # 列标题左对齐
        pd.set_option('display.max_colwidth', None) # 不限制列宽
        # 将 DataFrame 转换为字符串并打印，不显示索引，左对齐
        print(df.to_string(index=False, justify='left'))
    except Exception as e:
        # 如果使用 Pandas 显示出错，记录错误并回退到简单打印
        logging.error(f"使用 Pandas 显示结果时出错：{e}")
        for metric in metrics_list:
            print(metric)

# --- 主执行逻辑 ---
if __name__ == "__main__":
    # 定义配置文件和文档文件的路径 (相对于脚本运行位置)
    config_file = './standard/config/config.json'
    doc_file = './standard/data/NY 760-2004.docx'

    # 加载配置
    config = load_config(config_file)

    # 确保配置加载成功且文档文件存在
    if config and os.path.exists(doc_file):
        logging.info(f"--- 开始提取：{doc_file} ---")
        final_metrics = None # 存储最终要显示的指标
        source_description = "N/A" # 最终指标的来源描述

        # --- 步骤 1：脚本提取 ---
        script_metrics = [] # 初始化脚本结果列表
        try:
            logging.info(f"--- 步骤 1：基于 '{config_file}' 运行脚本提取 ---")
            # 加载 docx 文档对象
            doc = docx.Document(doc_file)
            # 调用核心提取函数
            script_metrics = extract_metrics_with_script(doc, config)
            # 显示脚本提取结果
            display_results(script_metrics, "[正则匹配]脚本规则提取")
        except Exception as e:
            # 记录脚本提取过程中的严重错误
            logging.error(f"脚本提取过程中出现严重错误：{e}", exc_info=True)
            # 即使出错，也要确保 script_metrics 是列表，以便后续检查

        # --- 步骤 2：检查是否需要 LLM 回退 ---
        logging.info(f"--- 步骤 2：检查回退阈值 ---")
        # 从配置获取阈值，提供默认值
        threshold = config.get('fallback_threshold', 5)
        # 计算脚本找到的有效指标数量（有名称和非空/非'-'的值）
        valid_script_metrics_count = sum(1 for m in script_metrics if m.get('指标名称') and m.get('指标数值') and m.get('指标数值') != '-')
        logging.info(f"脚本找到 {valid_script_metrics_count} 个有效指标。阈值 = {threshold}。")

        # --- 决定最终结果来源 ---
        # 检查是否需要调用 LLM (全局开关和阈值判断)
        if NEED_CALL_LLM_API and valid_script_metrics_count < threshold:
            logging.info(f"脚本结果低于阈值。尝试回退到文本模式（markdown 转换失败）...")
            # --- 步骤 3 & 4：调用 LLM API 并解析响应 (包含错误处理) ---
            try:
                logging.debug("进入 LLM 回退块。")
                print("--- 步骤 3：调用回退函数 ---") # 通知用户
                # 调用 API 函数
                llm_response_text = call_llm_api(doc_file, config)

                # 如果 API 调用成功返回了文本
                if llm_response_text:
                    logging.debug("收到响应。尝试解析。")
                    print("--- 步骤 4：解析响应 ---") # 通知用户
                    # 解析 LLM 响应
                    llm_metrics = parse_llm_response(llm_response_text)
                    # 如果解析成功得到指标列表
                    if llm_metrics:
                        final_metrics = llm_metrics # 最终结果使用 LLM 的结果
                        # source_description = f"LLM ({config.get('model_name')}) 提取"
                        source_description = f"脚本解析提取"

                        logging.info("回退成功。使用结果。")
                        # 立即显示 LLM 结果，以便用户看到
                        display_results(final_metrics, source_description)
                    else:
                        # LLM 响应无法解析
                        logging.warning("无法解析响应。保留脚本结果（如果有）。")
                        final_metrics = script_metrics # 保留脚本结果
                        source_description = "脚本规则提取 (解析失败)"
                else:
                    # LLM API 调用失败或未返回文本
                    logging.warning("调用失败或未返回文本。保留脚本结果（如果有）。")
                    final_metrics = script_metrics # 保留脚本结果
                    source_description = "脚本规则提取 (调用失败)"

            except Exception as e:
                # 捕获 LLM 回退过程中的意外错误
                logging.error(f"回退尝试期间出现意外错误：{e}", exc_info=True)
                final_metrics = script_metrics # 保留脚本结果
                source_description = "脚本规则提取 (过程出错)"
        else:
            # 不需要调用 LLM (或者全局开关关闭)
            if not NEED_CALL_LLM_API:
                logging.info("LLM API 调用已禁用 (NEED_CALL_LLM_API=False)。使用脚本结果。")
            else: # valid_script_metrics_count >= threshold
                logging.info("脚本结果达到或超过阈值。使用脚本结果。")
            final_metrics = script_metrics # 最终结果使用脚本结果
            source_description = "脚本规则提取"

        # --- 最终显示 ---
        # 如果最终结果来自脚本（包括 LLM 失败的情况），则再次显示最终确认结果
        # 如果 LLM 成功，则结果已在上面显示过
        if source_description.startswith("脚本规则提取"):
            print("\n--- 最终确认使用指标 ---")
            display_results(final_metrics, source_description)

    # --- 处理配置或文件加载失败的情况 ---
    elif not config:
        logging.error("没有有效的配置，脚本无法运行。")
    else: # config loaded but file not found
        logging.error(f"未找到输入文档 '{doc_file}'。")

    logging.info("--- 脚本执行完毕。 ---")